/*******************************************************************************
 * Copyright (c) 2006, 2016 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.service;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.internal.DsfPlugin;
import org.eclipse.cdt.dsf.internal.LoggingUtils;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.osgi.framework.Filter;

/**
 * Class to manage DSF sessions.  A DSF session is a way to 
 * associate a set of DSF services that are running simultaneously and 
 * are interacting with each other to provide a complete set of functionality.
 * <p>
 * Properties of a session are following:
 * <br>1. Each session is associated with a single DSF executor, although there
 * could be multiple sessions using the same executor.
 * <br>2. Each session has a unique String identifier, which has to be used by
 * the services belonging to this session when registering with OSGI services.
 * <br>3. Each session has its set of service event listeners.
 * <br>4. Start and end of each session is announced by events, which are always
 * sent on that session's executor dispatch thread.      
 * 
 * @see org.eclipse.cdt.dsf.concurrent.DsfExecutor
 * 
 * @since 1.0
 */
@ConfinedToDsfExecutor("getExecutor") 
public class DsfSession 
{
	/**
	 * Has the "debug/session" tracing option been turned on? Requires "debug"
	 * to also be turned on.
	 * 
	 * @since 2.1
	 */
	private static final boolean DEBUG_SESSION;

	/**
	 * Has the "debug/session/listeners" tracing option been turned on? Requires
	 * "debug/session" to also be turned on.
	 * 
	 * @since 2.1
	 */
    private static final boolean DEBUG_SESSION_LISTENERS;

	/**
	 * Has the "debug/session/dispatches" tracing option been turned on? Requires
	 * "debug/session" to also be turned on.
	 * 
	 * @since 2.1
	 */
    private static final boolean DEBUG_SESSION_DISPATCHES;

	/**
	 * Has the "debug/session/modelAdapters" tracing option been turned on? Requires
	 * "debug/session" to also be turned on.
	 * 
	 * @since 2.1
	 */
    private static final boolean DEBUG_SESSION_MODELADAPTERS;

    static {
    	DEBUG_SESSION = DsfPlugin.DEBUG && Boolean.parseBoolean(
                Platform.getDebugOption("org.eclipse.cdt.dsf/debug/session")); //$NON-NLS-1$
    	DEBUG_SESSION_LISTENERS = DEBUG_SESSION && Boolean.parseBoolean(
            Platform.getDebugOption("org.eclipse.cdt.dsf/debug/session/listeners")); //$NON-NLS-1$
    	DEBUG_SESSION_DISPATCHES = DEBUG_SESSION && Boolean.parseBoolean(
                Platform.getDebugOption("org.eclipse.cdt.dsf/debug/session/dispatches")); //$NON-NLS-1$
    	DEBUG_SESSION_MODELADAPTERS = DEBUG_SESSION && Boolean.parseBoolean(
    	        Platform.getDebugOption("org.eclipse.cdt.dsf/debug/session/modelAdapters")); //$NON-NLS-1$
    }  
	
    /** 
     * Listener for session started events.  This listener is always going to be
     * called in the dispatch thread of the session's executor.  
     */
    public static interface SessionStartedListener {
        /** 
         * Called when a new session is started.  It is always called in the 
         * dispatch thread of the new session.
         */
        public void sessionStarted(DsfSession session);
    }

    /** 
     * Listener for session ended events.  This listener is always going to be
     * called in the dispatch thread of the session's executor. 
     */
    public static interface SessionEndedListener {
        /** 
         * Called when a session is ended.  It is always called in the 
         * dispatch thread of the session.
         */
        public void sessionEnded(DsfSession session);
    }

    private static int fgSessionIdCounter = 0; 
    private static Set<DsfSession> fgActiveSessions = Collections.synchronizedSet(new HashSet<DsfSession>());
    private static List<SessionStartedListener> fSessionStartedListeners = Collections.synchronizedList(new ArrayList<SessionStartedListener>());
    private static List<SessionEndedListener> fSessionEndedListeners = Collections.synchronizedList(new ArrayList<SessionEndedListener>());
    
    /** Returns true if given session is currently active */
    public static boolean isSessionActive(String sessionId) {
        return getSession(sessionId) != null;
    }
    
    /** Returns a session instance for given session identifier */
    @ThreadSafe
    public static DsfSession getSession(String sessionId) {
        synchronized(fgActiveSessions) {
            for (DsfSession session : fgActiveSessions) {
                if (session.getId().equals(sessionId)) {
                    return session;
                }
            } 
        }
        return null;
    }

	/**
	 * Returns the active sessions
	 * 
	 * @since 2.1
	 */
	@ThreadSafe
	public static DsfSession[] getActiveSessions() {
		return fgActiveSessions.toArray(new DsfSession[fgActiveSessions.size()]);
	}

    /** 
     * Registers a listener for session started events.
     * Can be called on any thread. 
     */
    @ThreadSafe
    public static void addSessionStartedListener(SessionStartedListener listener) {
        assert !fSessionStartedListeners.contains(listener);
        fSessionStartedListeners.add(listener);
    }
    
    /** 
     * Un-registers a listener for session started events. 
     * Can be called on any thread. 
     */ 
    @ThreadSafe
    public static void removeSessionStartedListener(SessionStartedListener listener) {
        assert fSessionStartedListeners.contains(listener);
        fSessionStartedListeners.remove(listener);
    }
    
    /** 
     * Registers a listener for session ended events. 
     * Can be called on any thread. 
     */ 
    @ThreadSafe
    public static void addSessionEndedListener(SessionEndedListener listener) {
        assert !fSessionEndedListeners.contains(listener);
        fSessionEndedListeners.add(listener);
    }

    /** 
     * Un-registers a listener for session ended events. 
     * Can be called on any thread. 
     */ 
    @ThreadSafe
    public static void removeSessionEndedListener(SessionEndedListener listener) {
        assert fSessionEndedListeners.contains(listener);
        fSessionEndedListeners.remove(listener);
    }

    /**
     * Starts and returns a new session instance.  This method can be called on any
     * thread, but the session-started listeners will be called using the session's 
     * executor.
     * @param executor The DSF executor to use for this session.
     * @param ownerId ID (plugin ID preferably) of the owner of this session
     * @return instance object of the new session
     */
    @ThreadSafe
    public static DsfSession startSession(DsfExecutor executor, String ownerId) {
        synchronized(fgActiveSessions) {
            final DsfSession newSession = new DsfSession(executor, ownerId, Integer.toString(fgSessionIdCounter++));
            fgActiveSessions.add(newSession);
            executor.submit( new DsfRunnable() { @Override public void run() {
                SessionStartedListener[] listeners = fSessionStartedListeners.toArray(
                    new SessionStartedListener[fSessionStartedListeners.size()]);
                for (int i = 0; i < listeners.length; i++) {
                    listeners[i].sessionStarted(newSession);
                }
            }});
            return newSession;
        }
    }

    /**
     * Terminates the given session.  This method can be also called on any
     * thread, but the session-ended listeners will be called using the session's 
     * executor.
     * @param session session to terminate
     */
    @ThreadSafe
    public static void endSession(final DsfSession session) {
        synchronized(fgActiveSessions) {
            if (!fgActiveSessions.contains(session)) {
                throw new IllegalArgumentException();
            }
            fgActiveSessions.remove(session);
            session.getExecutor().submit( new DsfRunnable() { @Override public void run() {
                SessionEndedListener[] listeners = fSessionEndedListeners.toArray(
                    new SessionEndedListener[fSessionEndedListeners.size()]);
                for (int i = 0; i < listeners.length; i++) {
                    listeners[i].sessionEnded(session);
                }
            }});
        }
    }

    private static class ListenerEntry {
        Object fListener;
        Filter fFilter;

        ListenerEntry(Object listener, Filter filter) {
            fListener = listener;
            fFilter = filter;
        }
        
        @Override
        public boolean equals(Object other) {
            return other instanceof ListenerEntry && fListener.equals(((ListenerEntry)other).fListener);
        }
        
        @Override
        public int hashCode() { return fListener.hashCode(); }
    }

    /** ID (plugin ID preferably) of the owner of this session */
    private final String fOwnerId;
    
    /** Session ID of this session. */
    private final String fId;
    
    /** Dispatch-thread executor for this session */
    private final DsfExecutor fExecutor;
    
    /** Service start-up counter for this session */ 
    private int fServiceInstanceCounter;
    
    /** Map of registered event listeners. */
    private Map<ListenerEntry,Method[]> fListeners = new HashMap<ListenerEntry,Method[]>();
    
    /** 
     * Map of registered adapters, for implementing the <code>IDMContext.getAdapter()</code> 
     * method.
     * @see org.eclipse.cdt.dsf.datamodel.AbstractDMContext#getAdapter 
     */
    private Map<Class<?>,Object> fAdapters = Collections.synchronizedMap(new HashMap<Class<?>,Object>());

    /** Returns the owner ID of this session */
    @ThreadSafe
    public String getOwnerId() { return fOwnerId; }    
    
    public boolean isActive() { return DsfSession.isSessionActive(fId); }
    
    /** Returns the ID of this session */
    @ThreadSafe
    public String getId() { return fId; }
    
    /** Returns the DSF executor of this session */
    @ThreadSafe
    public DsfExecutor getExecutor() { return fExecutor; }

	/**
	 * Adds a new listener for service events in this session.  If the given 
	 * object is already registered as a listener, then this call does nothing.
	 * 
	 * <p>
	 * Listeners don't implement any particular interfaces. They declare one or
	 * more methods that are annotated with '@DsfServiceEventHandler', and which
	 * take a single event parameter. The type of the parameter indicates what
	 * events the handler is interested in. Any event that can be cast to that
	 * type (and which meets the optional filter) will be sent to it.
	 * 
	 * @param listener
	 *            the listener that will receive service events.
	 * @param filter
	 *            optional filter to restrict the services that the listener
	 *            will receive events from
	 */
    public void addServiceEventListener(Object listener, Filter filter) {
        assert getExecutor().isInExecutorThread();
        
        ListenerEntry entry = new ListenerEntry(listener, filter);
        if (DEBUG_SESSION_LISTENERS) {
        	Formatter formatter = new Formatter();
        	String msg = formatter.format(
        			"%s %s added as a service listener to %s (id=%s)", //$NON-NLS-1$
        			DsfPlugin.getDebugTime(),
        			LoggingUtils.toString(listener),
        			LoggingUtils.toString(this),
        			getId()
        			).toString();
        	formatter.close();

        	DsfPlugin.debug(msg);
        }
        fListeners.put(entry, getEventHandlerMethods(listener));
    }
    
    /**
     * Removes the given listener.  If the given object is not registered as a 
     * listener, then this call does nothing.
     * 
     * @param listener listener to remove
     */
    public void removeServiceEventListener(Object listener) {
        assert getExecutor().isInExecutorThread();

        ListenerEntry entry = new ListenerEntry(listener, null);
        if (DEBUG_SESSION_LISTENERS) {
        	Formatter formatter = new Formatter();
        	String msg = formatter.format(
        			"%s %s removed as a service listener to %s (id=%s)", //$NON-NLS-1$
        			DsfPlugin.getDebugTime(),
        			LoggingUtils.toString(listener),
        			LoggingUtils.toString(this),
        			getId()
        			).toString();
        	formatter.close();

        	DsfPlugin.debug(msg);
        }
        fListeners.remove(entry);
    }

    /**
     * Retrieves and increments the startup counter for services in this session.
     * DSF services should retrieve this counter when they are initialized, 
     * and should return it through IService.getStartupNumber().  This number is then
     * used to prioritize service events.
     * @return current startup counter value
     */
    public int getAndIncrementServiceStartupCounter() { return fServiceInstanceCounter++; }
    
    /**
     * Dispatches the given event to service event listeners.  The event is submitted to 
     * the executor to be dispatched.
     * @param event to be sent out
     * @param serviceProperties properties of the service requesting the event to be dispatched
     */
    @ThreadSafe
    public void dispatchEvent(final Object event, final Dictionary<?,?> serviceProperties) {
    	assert event != null;
        if (DEBUG_SESSION_DISPATCHES) {
        	Formatter formatter = new Formatter();
        	String msg = formatter.format(
        			"%s Dispatching event %s to session %s from thread \"%s\" (%d)", //$NON-NLS-1$
        			DsfPlugin.getDebugTime(),
        			LoggingUtils.toString(event),
        			LoggingUtils.toString(this),
        			Thread.currentThread().getName(),
        			Thread.currentThread().getId()
        			).toString();
        	formatter.close();
        	
        	DsfPlugin.debug(msg);
        }
        getExecutor().submit(new DsfRunnable() { 
            @Override
            public void run() { doDispatchEvent(event, serviceProperties);}
            @Override
            public String toString() { return "Event: " + event + ", from service " + serviceProperties; }  //$NON-NLS-1$ //$NON-NLS-2$
            });
    }
    
    /**
     * Registers a <code>IDMContext</code> adapter of given type.
     * @param adapterType class type to register the adapter for
     * @param adapter adapter instance to register
     * @see org.eclipse.dsdp.model.AbstractDMContext#getAdapter
     */
    @ThreadSafe
    public void registerModelAdapter(Class<?> adapterType, Object adapter) {
        if (DEBUG_SESSION_MODELADAPTERS) {
        	Formatter formatter = new Formatter();
        	String msg = formatter.format(
        			"%s Registering model adapter %s of type %s to session %s (%s)", //$NON-NLS-1$
        			DsfPlugin.getDebugTime(),
        			LoggingUtils.toString(adapter),
        			adapterType.getName(),
        			LoggingUtils.toString(this),
        			getId()
        			).toString();
        	formatter.close();
        	
        	DsfPlugin.debug(msg);
        }
    	
        fAdapters.put(adapterType, adapter);
    }
    
    /**
     * Un-registers a <code>IDMContext</code> adapter of given type.
     * @param adapterType adapter type to unregister
     * @see org.eclipse.dsdp.model.AbstractDMContext#getAdapter
     */
    @ThreadSafe
    public void unregisterModelAdapter(Class<?> adapterType) {
        if (DEBUG_SESSION_MODELADAPTERS) {
        	Formatter formatter = new Formatter();
        	String msg = formatter.format(
        			"%s Unregistering model adapter of type %s from session %s (%s)", //$NON-NLS-1$
        			DsfPlugin.getDebugTime(),
        			adapterType.getName(),
        			LoggingUtils.toString(this),
        			getId()
        			).toString();
        	formatter.close();
        	
        	DsfPlugin.debug(msg);
        }
    	
        fAdapters.remove(adapterType);
    }
    
    /** 
     * Retrieves an adapter for given type for <code>IDMContext</code>.
     * @param adapterType adapter type to look fors
     * @return adapter object for given type, null if none is registered with the session
     * @see org.eclipse.dsdp.model.AbstractDMContext#getAdapter
     */
    @ThreadSafe
    public Object getModelAdapter(Class<?> adapterType) {
        return fAdapters.get(adapterType);
    }
    
    @Override
    @ThreadSafe
    public boolean equals(Object other) {
        return other instanceof DsfSession && fId.equals(((DsfSession)other).fId);
    }
    
    @Override
    @ThreadSafe
    public int hashCode() { return fId.hashCode(); }

    private void doDispatchEvent(Object event, Dictionary<?,?> _serviceProperties) {
        // Need to cast to dictionary with String keys to satisfy OSGI in platform 3.7.
        // Bug 326233
        @SuppressWarnings("unchecked") 
        Dictionary<String,?> serviceProperties = (Dictionary<String,?>)_serviceProperties;
        
        // Build a list of listeners;
        SortedMap<ListenerEntry,List<Method>> listeners = new TreeMap<ListenerEntry,List<Method>>(new Comparator<ListenerEntry>() {
            @Override
                public int compare(ListenerEntry o1, ListenerEntry o2) {
                    if (o1.fListener == o2.fListener) {
                        return 0;
                    } if (o1.fListener instanceof IDsfService && !(o2.fListener instanceof IDsfService)) {
                        return -1;
                    } else if (o2.fListener instanceof IDsfService && !(o1.fListener instanceof IDsfService)) {
                        return 1;
                    } else if ( (o1.fListener instanceof IDsfService) && (o2.fListener instanceof IDsfService) ) {
                        return ((IDsfService)o1.fListener).getStartupNumber() - ((IDsfService)o2.fListener).getStartupNumber();
                    }
                    return 1;
                }
                
                @Override
                public boolean equals(Object obj) {
                    return obj == this;
                }
            });

        // Build a list of listeners and methods that are registered for this event class.
        Class<?> eventClass = event.getClass();
        for (Map.Entry<ListenerEntry,Method[]> entry : fListeners.entrySet()) {
            if (entry.getKey().fFilter != null && !entry.getKey().fFilter.match(serviceProperties)) {
                // Dispatching service doesn't match the listener's filter, skip it.
                continue;
            }
            Method[] allMethods = entry.getValue();
            List<Method> matchingMethods = new ArrayList<Method>();
            for (Method method : allMethods) {
            	assert method.getParameterTypes().length > 0 : eventClass.getName() + "." + method.getName() //$NON-NLS-1$
            		+ " signature contains zero parameters"; //$NON-NLS-1$
    	    	if ( method.getParameterTypes()[0].isAssignableFrom(eventClass) ) {
                    matchingMethods.add(method);
                }
            }
            if (!matchingMethods.isEmpty()) {
                listeners.put(entry.getKey(), matchingMethods);
            }
        }
        
        // Call the listeners
        for (Map.Entry<ListenerEntry,List<Method>> entry : listeners.entrySet()) {
            for (Method method : entry.getValue()) {
                try {
                    if (DEBUG_SESSION_DISPATCHES) {
                    	DsfPlugin.debug(DsfPlugin.getDebugTime() + " Listener " + LoggingUtils.toString(entry.getKey().fListener) + " invoked with event " + LoggingUtils.toString(event));  //$NON-NLS-1$ //$NON-NLS-2$
                    }
                    method.invoke(entry.getKey().fListener, new Object[] { event } );
                }
                catch (IllegalAccessException e) {
                    DsfPlugin.getDefault().getLog().log(new Status(
                        IStatus.ERROR, DsfPlugin.PLUGIN_ID, -1, "Security exception when calling a service event handler method", e)); //$NON-NLS-1$
                    assert false : "IServiceEventListener.ServiceHandlerMethod method not accessible, is listener declared public?"; //$NON-NLS-1$
                }
                catch (InvocationTargetException e) {
                    DsfPlugin.getDefault().getLog().log(new Status(
                        IStatus.ERROR, DsfPlugin.PLUGIN_ID, -1, "Invocation exception when calling a service event handler method", e)); //$NON-NLS-1$
                    assert false : "Exception thrown by a IServiceEventListener.ServiceHandlerMethod method"; //$NON-NLS-1$
                }
            }
        }
    }

	/**
	 * DSF event handlers don't implement any particular interfaces. They
	 * declare one or more methods that are annotated with
	 * '@DsfServiceEventHandler', and which take a single event parameter. The
	 * type of the parameter indicates what events the handler is interested in.
	 * Any event that can be cast to that type (and which meets the optional
	 * filter provided when the listener is registered) will be sent to it.
	 * 
	 * This method returns the methods annotated as handlers. Each method is
	 * checked to ensure it takes a single parameter; an
	 * {@link IllegalArgumentException} is thrown otherwise.
	 * 
	 * @param listener
	 *            an object which should contain handler methods
	 * @return the collection of handler methods
	 */
    private Method[] getEventHandlerMethods(Object listener) 
    {
        List<Method> retVal = new ArrayList<Method>();
        try {
            Method[] methods = listener.getClass().getMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(DsfServiceEventHandler.class)) {
                    Class<?>[] paramTypes = method.getParameterTypes();
                    if (paramTypes.length != 1) {	// must have one and only param
                        throw new IllegalArgumentException("@DsfServiceEventHandler method has incorrect number of parameters"); //$NON-NLS-1$
                    } 
                    retVal.add(method);
                }
            }
        } catch(SecurityException e) {
            throw new IllegalArgumentException("No permission to access @DsfServiceEventHandler method"); //$NON-NLS-1$
        }
        
        if (retVal.isEmpty()) {
            throw new IllegalArgumentException("No methods annotated with @DsfServiceEventHandler in listener, is listener declared public?"); //$NON-NLS-1$
        }
        return retVal.toArray(new Method[retVal.size()]);
    }
    
    /**
     * Class to be instantiated only using startSession()
     */
    @ThreadSafe
    private DsfSession(DsfExecutor executor, String ownerId, String id) {
        fId = id;
        fOwnerId = ownerId;
        fExecutor = executor;
    }
}
